<?php
namespace DRY\Module;

/**
 * DonReY Framework 2012 :: Data Persistence module (data survives between script runs)
 * Used to store data in a persistent way, so it will be available after the script ends execution
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/persistence.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Persistence
 */
class Persistence extends base
{
	const
		Autoload_Directory = 'persistence',
		Autoload_Prefix = 'DRY\Persistence\Container',
		Autoload_Suffix = 'container'
		;

	const
		EXC_CONTAINER_INIT_FAIL = 1;

	/**
	 * Contains references to all created continers
	 *
	 * @var array[DRY_Persistence_Container]
	 */
	protected $_containers;

	/**
	 * Creates a new storage container of a certain type
	 *
	 * @param string $name How the container is called
	 * @param string $type Type of the container (default: session)
	 * @param string $owner_id ID of the owner of this container
	 * @return persistence_container Container object of a derived class from persistence_container
	 */
	public function &factory_Container($name, $type='session', $owner_id=null)
	{
		// if the container already exists, use it
		if(isset($this->_containers[$name]))
			return $this->_containers[$name];

		// include the required class
		if(!isset($this-> CFG->$type))
			$type = 'session'; // disable other types if configuration for them doesn't exist

		// else, create it
		$class_name = self::Autoload_Prefix.'_'.ucfirst($type);
		if(class_exists($class_name, true) && $class_name::global_Init()==true) { // create container only if global init succeeded
			// create the container object
			$this->_containers[$name] = new $class_name($name, $this->_alias, $this-> CFG->$type, $owner_id); // Not all drivers implement all the constructor parameters (Session for example uses just the $name)
			return $this->_containers[$name];
		} else
			// class doesn't exist or was unable to perform global_init() on the storage engine, cannot create container
			throw new \DRY\Water("persistence", self::EXC_CONTAINER_INIT_FAIL, array('type'=>$type));
	}

	/**
	 * Overloader for getting existing container by name
	 *
	 * @param string $name
	 */
	public function &__get($name)
	{
		if(isset($this->_containers[$name]))
			return $this->_containers[$name];
		else {
			return $this-> factory_Container($name); // auto-creates the container at first use
			// TODO: Config-based default settings ( right now all auto-containers are "session" )
		}
	}

	/**
	 * Overloader to check if a container name exists
	 *
	 * @param string $name
	 * @return bool
	 */
	public function __isset($name)
	{
		if(isset($this->_containers[$name]))
			return true;
		else
			return false;
	}

	/**
	 * Removes a container from memory.
	 * This does not delete the persistent data it is holding, only frees up the memory for the container object.
	 * The side effect is that the container data is automatically saved by the __destruct function of the container
	 *
	 * @param string $name
	 */
	public function __unset($name)
	{
		unset($this->_containers[$name]);
	}

	/**
	 * Self cleans by removing the references to all container objects
	 */
	public function self_clean()
	{
		unset($this->_containers);
	}
}

namespace DRY\Persistence;

interface Container
{
	/**
	 * First initialization of a container type
	 * ( for example, "session" container needs "session_start" once, even for multiple containers )
	 */
	public static function global_Init();

	/**
	 * All containers must have a destructor that save the data from the container into their storage place
	 */
	function __destruct();

	/**
	 * Magic function for reading data inside container using ->varname
	 *
	 * @param string $varname
	 * @return mixed Data from the variable
	 */
	public function &__get($varname);

	/**
	 * Magic function for writing data to container using ->varname
	 *
	 * @param string $varname
	 */
	public function __set($varname, $value);

	/**
	 * Magic function for checking if a variable is part of the container
	 *
	 * @param string $varname
	 * @return bool True if the variable does exist
	 */
	public function __isset($varname);

	/**
	 * Magic function for removing a variable from the container
	 *
	 * @param string $varname
	 */
	public function __unset($varname);

	/**
	 * Removes the persistent data for this container ( practically empties the container )
	 */
	public function destroy();

	/**
	 * Returns the entire content of the container ( all variables ) into an array
	 *
	 * @return array Content of container
	 */
	public function &read_All();
}

/**
 * Base class for data containers. Derived classes will implement a method of storing the data
 *
 */
abstract class Container_base implements Container, \Countable
{
	/**
	 * Flag related to global initialization of storage engines
	 *
	 * @var array[bool]
	 */
	static $_initialized;

	/**
	 * Container name, used in all operations
	 *
	 * @var string
	 */
	protected $_name;

	/**
	 * Container constructor. Also timeout and hoplimit handler
	 * NOTE: Since the hops left are decreased only when this constructor is called, if the container is not factoried,
	 *       the hops are not decreased, even if the script gets reloaded.
	 *
	 * Full version for derived constructors: function __construct($container_name, $persistence_module_alias, $container_type_CFG, $owner_id)
	 *
	 * @param string $name
	 */
	public function __construct($name)
	{
		$this->_name=$name;

		// parses the expiry special var
		if(isset($this->__expiry) && is_array($this->__expiry)) {
			foreach($this->__expiry as $exp_key=>$exp_value) {
				$varname = substr($exp_key,3);
				if(substr($exp_key,0,3)=='to_') { // timeout
					if(time()>$exp_value) {
						if(strlen($varname)==0) // no var specified in expiry, which means the entire container expires
							$this-> destroy();
						else
							unset($this->$varname, $this->__expiry[$exp_key], $this->__expiry['hl_'.$varname]); // also expire hops
					}
				}
				if(substr($exp_key,0,3)=='hl_') { // hoplimit
					$this->__expiry[$exp_key]--;
					if($exp_value<=0) {
						if(strlen($varname)==0) // ... same as above ...
							$this-> destroy();
						else
							unset($this->$varname, $this->__expiry[$exp_key], $this->__expiry['to_'.$varname]); // also expire time
					}
				}
			}
			// if there's nothing else to expire, remove the __expiry index as well
			if(is_null($this->__expiry) || empty($this->__expiry))
				unset($this->__expiry);
		}
	}

//===== Expiry functions =====//

	/**
	 * Sets a variable or the entire container to expire after a number of seconds. If expire time has been reached, the data is destroyed
	 *
	 * @param int+ $time
	 * @param string $varname
	 * @return bool false if some error occured
	 */
	public function timeout($time, $varname=null)
	{
		if($time<=0 || (!is_null($varname) && !isset($this->$varname)))
			return false; // error, varname doesn't exist or time is not positive number

		$this->__expiry["to_".$varname] = time()+$time;
		return true;
	}

	/**
	 * Returns the time left until the variable expires
	 *
	 * @param string $varname
	 * @return int Time left
	 */
	public function get_Time_Left($varname=null)
	{
		if(!is_null($varname) && !isset($this->$varname))
			return false; // error, varname doesn't exist

		if(isset($this->__expiry["to_".$varname]))
			return($this->__expiry["to_".$varname]-time());
	}

	/**
	 * Sets a variable or the entire container to expire after the container has been loaded a number of times. If so, the data is destroyed
	 *
	 * @param int+ $hops
	 * @param string $varname
	 * @return bool false if some error occured
	 */
	public function hop_Limit($hops, $varname=null)
	{
		if($hops<=0 || (!is_null($varname) && !isset($this->$varname)))
			return false; // error, varname doesn't exist or time is not positive number

		$this->__expiry["hl_".$varname] = $hops;
		return true;
	}

	/**
	 * Returns the hops left until the variable expires
	 *
	 * @param string $varname
	 * @return int Hops left
	 */
	public function get_Hops_Left($varname=null)
	{
		if(!is_null($varname) && !isset($this->$varname))
			return false; // error, varname doesn't exist

		if(isset($this->__expiry) && isset($this->__expiry["hl_".$varname]))
			return($this->__expiry["hl_".$varname]);
	}

}